/**
 * Copyright (C) 2001-2020 by RapidMiner and the contributors
 *
 * Complete list of developers available at our web site:
 *
 * http://rapidminer.com
 *
 * This program is free software: you can redistribute it and/or modify it under the terms of the
 * GNU Affero General Public License as published by the Free Software Foundation, either version 3
 * of the License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without
 * even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
 * Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License along with this program.
 * If not, see http://www.gnu.org/licenses/.
 */
package com.rapidminer.gui.tools.syntax;

/*
 * TextAreaPainter.java - Paints the text area Copyright (C) 1999 Slava Pestov
 *
 * You may use and modify this package for any purpose. Redistribution is permitted, in both source
 * and binary form, provided that this notice remains intact in all source distributions of this
 * package.
 */

import java.awt.Color;
import java.awt.Cursor;
import java.awt.Dimension;
import java.awt.Font;
import java.awt.FontMetrics;
import java.awt.Graphics;
import java.awt.Rectangle;
import java.awt.Toolkit;
import java.awt.event.MouseEvent;

import javax.swing.JComponent;
import javax.swing.ToolTipManager;
import javax.swing.text.Segment;
import javax.swing.text.TabExpander;
import javax.swing.text.Utilities;

import com.rapidminer.tools.FontTools;


/**
 * The text area repaint manager. It performs double buffering and paints lines of text.
 *
 * @author Slava Pestov, Ingo Mierswa
 */
public class TextAreaPainter extends JComponent implements TabExpander {

	private static final long serialVersionUID = 5512879422283716799L;

	/**
	 * Creates a new repaint manager. This should be not be called directly.
	 */
	public TextAreaPainter(JEditTextArea textArea, TextAreaDefaults defaults) {
		this.textArea = textArea;

		setAutoscrolls(true);
		setDoubleBuffered(true);
		setOpaque(true);

		ToolTipManager.sharedInstance().registerComponent(this);

		currentLine = new Segment();
		currentLineIndex = -1;

		setCursor(Cursor.getPredefinedCursor(Cursor.TEXT_CURSOR));

		setFont(FontTools.getFont(Font.MONOSPACED, Font.PLAIN, 14));
		setForeground(Color.black);
		setBackground(Color.white);

		blockCaret = defaults.blockCaret;
		styles = defaults.styles;
		cols = defaults.cols;
		rows = defaults.rows;
		caretColor = defaults.caretColor;
		selectionColor = defaults.selectionColor;
		lineHighlightColor = defaults.lineHighlightColor;
		lineHighlight = defaults.lineHighlight;
		bracketHighlightColor = defaults.bracketHighlightColor;
		bracketHighlight = defaults.bracketHighlight;
		paintInvalid = defaults.paintInvalid;
		eolMarkerColor = defaults.eolMarkerColor;
		eolMarkers = defaults.eolMarkers;
	}

	protected Object readResolve() {
		currentLine = new Segment();
		styles = TextAreaDefaults.getDefaults().styles;
		return this;
	}

	/**
	 * Returns if this component can be traversed by pressing the Tab key. This returns false.
	 */
	/*
	 * public final boolean isManagingFocus() { return false; }
	 */

	/**
	 * Returns the syntax styles used to paint colorized text. Entry <i>n</i> will be used to paint
	 * tokens with id = <i>n</i>.
	 */
	public final SyntaxStyle[] getStyles() {
		return styles;
	}

	/**
	 * Sets the syntax styles used to paint colorized text. Entry <i>n</i> will be used to paint
	 * tokens with id = <i>n</i>.
	 *
	 * @param styles
	 *            The syntax styles
	 */
	public final void setStyles(SyntaxStyle[] styles) {
		this.styles = styles;
		repaint();
	}

	/**
	 * Returns the caret color.
	 */
	public final Color getCaretColor() {
		return caretColor;
	}

	/**
	 * Sets the caret color.
	 *
	 * @param caretColor
	 *            The caret color
	 */
	public final void setCaretColor(Color caretColor) {
		this.caretColor = caretColor;
		invalidateSelectedLines();
	}

	/**
	 * Returns the selection color.
	 */
	public final Color getSelectionColor() {
		return selectionColor;
	}

	/**
	 * Sets the selection color.
	 *
	 * @param selectionColor
	 *            The selection color
	 */
	public final void setSelectionColor(Color selectionColor) {
		this.selectionColor = selectionColor;
		invalidateSelectedLines();
	}

	/**
	 * Returns the line highlight color.
	 */
	public final Color getLineHighlightColor() {
		return lineHighlightColor;
	}

	/**
	 * Sets the line highlight color.
	 *
	 * @param lineHighlightColor
	 *            The line highlight color
	 */
	public final void setLineHighlightColor(Color lineHighlightColor) {
		this.lineHighlightColor = lineHighlightColor;
		invalidateSelectedLines();
	}

	/**
	 * Returns true if line highlight is enabled, false otherwise.
	 */
	public final boolean isLineHighlightEnabled() {
		return lineHighlight;
	}

	/**
	 * Enables or disables current line highlighting.
	 *
	 * @param lineHighlight
	 *            True if current line highlight should be enabled, false otherwise
	 */
	public final void setLineHighlightEnabled(boolean lineHighlight) {
		this.lineHighlight = lineHighlight;
		invalidateSelectedLines();
	}

	/**
	 * Returns the bracket highlight color.
	 */
	public final Color getBracketHighlightColor() {
		return bracketHighlightColor;
	}

	/**
	 * Sets the bracket highlight color.
	 *
	 * @param bracketHighlightColor
	 *            The bracket highlight color
	 */
	public final void setBracketHighlightColor(Color bracketHighlightColor) {
		this.bracketHighlightColor = bracketHighlightColor;
		invalidateLine(textArea.getBracketLine());
	}

	/**
	 * Returns true if bracket highlighting is enabled, false otherwise. When bracket highlighting
	 * is enabled, the bracket matching the one before the caret (if any) is highlighted.
	 */
	public final boolean isBracketHighlightEnabled() {
		return bracketHighlight;
	}

	/**
	 * Enables or disables bracket highlighting. When bracket highlighting is enabled, the bracket
	 * matching the one before the caret (if any) is highlighted.
	 *
	 * @param bracketHighlight
	 *            True if bracket highlighting should be enabled, false otherwise
	 */
	public final void setBracketHighlightEnabled(boolean bracketHighlight) {
		this.bracketHighlight = bracketHighlight;
		invalidateLine(textArea.getBracketLine());
	}

	/**
	 * Returns true if the caret should be drawn as a block, false otherwise.
	 */
	public final boolean isBlockCaretEnabled() {
		return blockCaret;
	}

	/**
	 * Sets if the caret should be drawn as a block, false otherwise.
	 *
	 * @param blockCaret
	 *            True if the caret should be drawn as a block, false otherwise.
	 */
	public final void setBlockCaretEnabled(boolean blockCaret) {
		this.blockCaret = blockCaret;
		invalidateSelectedLines();
	}

	/**
	 * Returns the EOL marker color.
	 */
	public final Color getEOLMarkerColor() {
		return eolMarkerColor;
	}

	/**
	 * Sets the EOL marker color.
	 *
	 * @param eolMarkerColor
	 *            The EOL marker color
	 */
	public final void setEOLMarkerColor(Color eolMarkerColor) {
		this.eolMarkerColor = eolMarkerColor;
		repaint();
	}

	/**
	 * Returns true if EOL markers are drawn, false otherwise.
	 */
	public final boolean getEOLMarkersPainted() {
		return eolMarkers;
	}

	/**
	 * Sets if EOL markers are to be drawn.
	 *
	 * @param eolMarkers
	 *            True if EOL markers should be drawn, false otherwise
	 */
	public final void setEOLMarkersPainted(boolean eolMarkers) {
		this.eolMarkers = eolMarkers;
		repaint();
	}

	/**
	 * Returns true if invalid lines are painted as red tildes (~), false otherwise.
	 */
	public boolean getInvalidLinesPainted() {
		return paintInvalid;
	}

	/**
	 * Sets if invalid lines are to be painted as red tildes.
	 *
	 * @param paintInvalid
	 *            True if invalid lines should be drawn, false otherwise
	 */
	public void setInvalidLinesPainted(boolean paintInvalid) {
		this.paintInvalid = paintInvalid;
		repaint();
	}

	/**
	 * Adds a custom highlight painter.
	 *
	 * @param highlight
	 *            The highlight
	 */
	public void addCustomHighlight(Highlight highlight) {
		highlight.init(textArea, highlights);
		highlights = highlight;
	}

	/**
	 * Highlight interface.
	 */
	public interface Highlight {

		/**
		 * Called after the highlight painter has been added.
		 *
		 * @param textArea
		 *            The text area
		 * @param next
		 *            The painter this one should delegate to
		 */
		void init(JEditTextArea textArea, Highlight next);

		/**
		 * This should paint the highlight and delgate to the next highlight painter.
		 *
		 * @param gfx
		 *            The graphics context
		 * @param line
		 *            The line number
		 * @param y
		 *            The y co-ordinate of the line
		 */
		void paintHighlight(Graphics gfx, int line, int y);

		/**
		 * Returns the tool tip to display at the specified location. If this highlighter doesn't
		 * know what to display, it should delegate to the next highlight painter.
		 *
		 * @param evt
		 *            The mouse event
		 */
		String getToolTipText(MouseEvent evt);
	}

	/**
	 * Returns the tool tip to display at the specified location.
	 *
	 * @param evt
	 *            The mouse event
	 */
	@Override
	public String getToolTipText(MouseEvent evt) {
		if (highlights != null) {
			return highlights.getToolTipText(evt);
		} else {
			return null;
		}
	}

	/**
	 * Returns the font metrics used by this component.
	 */
	public FontMetrics getFontMetrics() {
		return fm;
	}

	/**
	 * Sets the font for this component. This is overridden to update the cached font metrics and to
	 * recalculate which lines are visible.
	 *
	 * @param font
	 *            The font
	 */
	@Override
	@SuppressWarnings("deprecation")
	public void setFont(Font font) {
		super.setFont(font);
		fm = Toolkit.getDefaultToolkit().getFontMetrics(font);
		textArea.recalculateVisibleLines();
	}

	/**
	 * Repaints the text.
	 *
	 * @param gfx
	 *            The graphics context
	 */
	@Override
	public void paint(Graphics gfx) {
		// tabSize = fm.charWidth(' ') * ((Integer)
		// textArea.getDocument().getProperty(PlainDocument.tabSizeAttribute)).intValue();
		tabSize = fm.charWidth(' ') * 4; // IM: fix this to 4 (which is nicer...)

		Rectangle clipRect = gfx.getClipBounds();

		gfx.setColor(getBackground());
		gfx.fillRect(clipRect.x, clipRect.y, clipRect.width, clipRect.height);

		// We don't use yToLine() here because that method doesn't
		// return lines past the end of the document
		int height = fm.getHeight();
		int firstLine = textArea.getFirstLine();
		int firstInvalid = firstLine + clipRect.y / height;
		// Because the clipRect's height is usually an even multiple
		// of the font height, we subtract 1 from it, otherwise one
		// too many lines will always be painted.
		int lastInvalid = firstLine + (clipRect.y + clipRect.height - 1) / height;

		try {
			TokenMarker tokenMarker = textArea.getDocument().getTokenMarker();
			int x = textArea.getHorizontalOffset();

			for (int line = firstInvalid; line <= lastInvalid; line++) {
				paintLine(gfx, tokenMarker, line, x);
			}

			if (tokenMarker != null && tokenMarker.isNextLineRequested()) {
				int h = clipRect.y + clipRect.height;
				repaint(0, h, getWidth(), getHeight() - h);
			}
		} catch (Exception e) {
			// System.err.println("Error repainting line" + " range {" + firstInvalid + "," +
			// lastInvalid + "}:");
			e.printStackTrace();
		}
	}

	/**
	 * Marks a line as needing a repaint.
	 *
	 * @param line
	 *            The line to invalidate
	 */
	public final void invalidateLine(int line) {
		repaint(0, textArea.lineToY(line) + fm.getMaxDescent() + fm.getLeading(), getWidth(), fm.getHeight());
	}

	/**
	 * Marks a range of lines as needing a repaint.
	 *
	 * @param firstLine
	 *            The first line to invalidate
	 * @param lastLine
	 *            The last line to invalidate
	 */
	public final void invalidateLineRange(int firstLine, int lastLine) {
		repaint(0, textArea.lineToY(firstLine) + fm.getMaxDescent() + fm.getLeading(), getWidth(),
				(lastLine - firstLine + 1) * fm.getHeight());
	}

	/**
	 * Repaints the lines containing the selection.
	 */
	public final void invalidateSelectedLines() {
		invalidateLineRange(textArea.getSelectionStartLine(), textArea.getSelectionEndLine());
	}

	/**
	 * Implementation of TabExpander interface. Returns next tab stop after a specified point.
	 *
	 * @param x
	 *            The x co-ordinate
	 * @param tabOffset
	 *            Ignored
	 * @return The next tab stop after <i>x</i>
	 */
	@Override
	public float nextTabStop(float x, int tabOffset) {
		int offset = textArea.getHorizontalOffset();
		int ntabs = ((int) x - offset) / tabSize;
		return (ntabs + 1) * tabSize + offset;
	}

	/**
	 * Returns the painter's preferred size.
	 */
	@Override
	public Dimension getPreferredSize() {
		Dimension dim = new Dimension();
		dim.width = fm.charWidth('w') * cols;
		dim.height = fm.getHeight() * rows;
		return dim;
	}

	/**
	 * Returns the painter's minimum size.
	 */
	/*
	 * public Dimension getMinimumSize() { return getPreferredSize(); }
	 */

	// package-private members
	int currentLineIndex;

	transient Token currentLineTokens;

	transient Segment currentLine;

	// protected members
	protected JEditTextArea textArea;

	protected transient SyntaxStyle[] styles;

	protected Color caretColor;

	protected Color selectionColor;

	protected Color lineHighlightColor;

	protected Color bracketHighlightColor;

	protected Color eolMarkerColor;

	protected boolean blockCaret;

	protected boolean lineHighlight;

	protected boolean bracketHighlight;

	protected boolean paintInvalid;

	protected boolean eolMarkers;

	protected int cols;

	protected int rows;

	protected int tabSize = 1;

	protected FontMetrics fm;

	protected transient Highlight highlights;

	protected void paintLine(Graphics gfx, TokenMarker tokenMarker, int line, int x) {
		Font defaultFont = getFont();
		Color defaultColor = getForeground();

		currentLineIndex = line;
		int y = textArea.lineToY(line);

		if (line < 0 || line >= textArea.getLineCount()) {
			if (paintInvalid) {
				paintHighlight(gfx, line, y);
				styles[Token.INVALID].setGraphicsFlags(gfx, defaultFont);
				gfx.drawString("~", 0, y + fm.getHeight());
			}
		} else if (tokenMarker == null) {
			paintPlainLine(gfx, line, defaultFont, defaultColor, x, y);
		} else {
			paintSyntaxLine(gfx, tokenMarker, line, defaultFont, defaultColor, x, y);
		}
	}

	protected void paintPlainLine(Graphics gfx, int line, Font defaultFont, Color defaultColor, int x, int y) {
		paintHighlight(gfx, line, y);
		textArea.getLineText(line, currentLine);

		gfx.setFont(defaultFont);
		gfx.setColor(defaultColor);

		y += fm.getHeight();
		x = Utilities.drawTabbedText(currentLine, x, y, gfx, this, 0);

		if (eolMarkers) {
			gfx.setColor(eolMarkerColor);
			gfx.drawString(".", x, y);
		}
	}

	protected void paintSyntaxLine(Graphics gfx, TokenMarker tokenMarker, int line, Font defaultFont, Color defaultColor,
			int x, int y) {
		textArea.getLineText(currentLineIndex, currentLine);
		currentLineTokens = tokenMarker.markTokens(currentLine, currentLineIndex);

		paintHighlight(gfx, line, y);

		gfx.setFont(defaultFont);
		gfx.setColor(defaultColor);
		y += fm.getHeight();
		x = SyntaxUtilities.paintSyntaxLine(currentLine, currentLineTokens, styles, this, gfx, x, y);

		if (eolMarkers) {
			gfx.setColor(eolMarkerColor);
			gfx.drawString(".", x, y);
		}
	}

	protected void paintHighlight(Graphics gfx, int line, int y) {
		if (line >= textArea.getSelectionStartLine() && line <= textArea.getSelectionEndLine()) {
			paintLineHighlight(gfx, line, y);
		}

		if (highlights != null) {
			highlights.paintHighlight(gfx, line, y);
		}

		if (bracketHighlight && line == textArea.getBracketLine()) {
			paintBracketHighlight(gfx, line, y);
		}

		if (line == textArea.getCaretLine()) {
			paintCaret(gfx, line, y);
		}
	}

	protected void paintLineHighlight(Graphics gfx, int line, int y) {
		int height = fm.getHeight();
		y += fm.getLeading() + fm.getMaxDescent();

		int selectionStart = textArea.getSelectionStart();
		int selectionEnd = textArea.getSelectionEnd();

		if (selectionStart == selectionEnd) {
			if (lineHighlight) {
				gfx.setColor(lineHighlightColor);
				gfx.fillRect(0, y, getWidth(), height);
			}
		} else {
			gfx.setColor(selectionColor);

			int selectionStartLine = textArea.getSelectionStartLine();
			int selectionEndLine = textArea.getSelectionEndLine();
			int lineStart = textArea.getLineStartOffset(line);

			int x1, x2;
			if (textArea.isSelectionRectangular()) {
				int lineLen = textArea.getLineLength(line);
				x1 = textArea._offsetToX(line,
						Math.min(lineLen, selectionStart - textArea.getLineStartOffset(selectionStartLine)));
				x2 = textArea._offsetToX(line,
						Math.min(lineLen, selectionEnd - textArea.getLineStartOffset(selectionEndLine)));
				if (x1 == x2) {
					x2++;
				}
			} else if (selectionStartLine == selectionEndLine) {
				x1 = textArea._offsetToX(line, selectionStart - lineStart);
				x2 = textArea._offsetToX(line, selectionEnd - lineStart);
			} else if (line == selectionStartLine) {
				x1 = textArea._offsetToX(line, selectionStart - lineStart);
				x2 = getWidth();
			} else if (line == selectionEndLine) {
				x1 = 0;
				x2 = textArea._offsetToX(line, selectionEnd - lineStart);
			} else {
				x1 = 0;
				x2 = getWidth();
			}

			// "inlined" min/max()
			gfx.fillRect(x1 > x2 ? x2 : x1, y, x1 > x2 ? x1 - x2 : x2 - x1, height);
		}

	}

	protected void paintBracketHighlight(Graphics gfx, int line, int y) {
		int position = textArea.getBracketPosition();
		if (position == -1) {
			return;
		}
		y += fm.getLeading() + fm.getMaxDescent();
		int x = textArea._offsetToX(line, position);
		gfx.setColor(bracketHighlightColor);
		// Hack!!! Since there is no fast way to get the character
		// from the bracket matching routine, we use ( since all
		// brackets probably have the same width anyway
		gfx.drawRect(x, y, fm.charWidth('(') - 1, fm.getHeight() - 1);
	}

	protected void paintCaret(Graphics gfx, int line, int y) {
		if (textArea.isCaretVisible()) {
			int offset = textArea.getCaretPosition() - textArea.getLineStartOffset(line);
			int caretX = textArea._offsetToX(line, offset);
			int caretWidth = blockCaret || textArea.isOverwriteEnabled() ? fm.charWidth('w') : 1;
			y += fm.getLeading() + fm.getMaxDescent();
			int height = fm.getHeight();

			gfx.setColor(caretColor);

			if (textArea.isOverwriteEnabled()) {
				gfx.fillRect(caretX, y + height - 1, caretWidth, 1);
			} else {
				gfx.drawRect(caretX, y, caretWidth - 1, height - 1);
			}
		}
	}
}
